Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional bitcoin_hashes feature to implement ThirtyTwoByteHash #206

Merged
merged 2 commits into from May 24, 2020

Conversation

sgeisler
Copy link
Contributor

@sgeisler sgeisler commented Apr 7, 2020

This PR adds bitcoin_hashes as an optional dependency to implement the ThirtyTwoByteHash trait for the relevant hash types. It also adds a shortcut function Message::from_hashed_data<H: Hash>(&[u8]).

Copy link
Contributor

@stevenroose stevenroose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks ok so far. I'd like to look a bit closer in the situations where this trait is used and how this affects the UX as a rust-secp user.

I might also be tempted to have the feature be default.

@@ -46,6 +46,10 @@ rand_core = "0.4"
serde_test = "1.0"
bitcoin_hashes = "0.7"

[dependencies.bitcoin_hashes]
version = "0.7"
optional = true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any arguments here against making that default? Annoyingly someone that is depending on bitcoin and depends on secp and hashes using bitcoin::secp256k1 and bitcoin::hashes can't add features to the secp dependency. Well, that's a broader Cargo problem, but well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this a lot 😅 (also because of features I might want to activate in secp256k1/bitcoin_hashes to not cause incompatibilities), turns out that cargo builds dependencies with the union of all the features activated if a dependency exists multiple times in the dependency graph (just tested it with a minimal example). So all they had to do was to specify secp256k1 as a dependency themselves and activate the feature.

On the other hand we could just activate that feature in bitcoin and use it there too. After taking a closer look we don't seem to have any transaction signing code? At least we don't use secp256k1::Message anywhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure? It might be that we use an into() and that Message is never mentioned verbatim. I add a usage of this in the Bitcoin Signed Message PR.


/// Constructs a `Message` by hashing `data` with hash algorithm `H`
#[cfg(feature = "bitcoin_hashes")]
pub fn from_hashed_data<H: ThirtyTwoByteHash + bitcoin_hashes::Hash>(data: &[u8]) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm how is a method like this used? By explicitly providing the type param? That might be a bit awkward UX?

let msg = Message::from_hashes_data<sha256::Hash>(&my_data[..]);

Or is there another way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that's a bit awkward, but imo still better than having a separate method for each hash function. Given the confusion I should definitely document it better since using a type parameter in such a way is not too common. Alternatively I could just remove it, Message::from(sha256::Hash::hash(&my_data)) will do the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just added an example to the docs. I personally find that, although a bit longer, the first version reads better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM :)

@sgeisler sgeisler force-pushed the 2020-04-hashes branch 2 times, most recently from 75291bf to c905de2 Compare April 8, 2020 10:28
@elichai
Copy link
Member

elichai commented Apr 9, 2020

I think this is interesting.
I would like to play with the ux of the function that hashes the data for you, maybe by passing in a hasher instead of just being generic over one.
Anyhow, I think an API that hashes for you is a safer API.

stevenroose
stevenroose previously approved these changes Apr 19, 2020
Copy link
Contributor

@stevenroose stevenroose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

src/lib.rs Outdated
#[cfg(feature = "bitcoin_hashes")]
pub fn from_hashed_data<H: ThirtyTwoByteHash + bitcoin_hashes::Hash>(data: &[u8]) -> Self {
let hash: H = bitcoin_hashes::Hash::hash(data);
Message::from(hash)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: What a missed chance for a cool one-liner!

<H as bitcoin_hashes::Hash>::hash(data).into()

Don't we all love Rust! :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, that's neat, let me sneak it in :P

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, didn't notice you have the trait imported

#[cfg(feature = "bitcoin_hashes")]
use bitcoin_hashes::Hash;

So it could be shorted. But nvm that.

Copy link
Contributor Author

@sgeisler sgeisler Apr 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually avoid that on purpose. It collides with std::hash::Hash too often in my experience. EDIT: thinking about it: where is this import needed? 😄


/// Constructs a `Message` by hashing `data` with hash algorithm `H`
#[cfg(feature = "bitcoin_hashes")]
pub fn from_hashed_data<H: ThirtyTwoByteHash + bitcoin_hashes::Hash>(data: &[u8]) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM :)

stevenroose
stevenroose previously approved these changes Apr 20, 2020
Copy link
Contributor

@stevenroose stevenroose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK 57526f8

@thomaseizinger
Copy link
Contributor

thomaseizinger commented Apr 24, 2020

I thought about the APIs in place here quite some time while implementing a PoC that uses the secp256k1 curve in ways other than just signing Bitcoin transactions and I found the Message API to be particularly constraining.

Here is a proposal for dealing with the whole ThirtyTwoByteHash and "how do we make rust-secp256k1 and bitcoin_hashes play nicely with each other" topic: #211

@sgeisler
Copy link
Contributor Author

sgeisler commented Apr 24, 2020

While your implementation looks technically sound and maybe even a bit more idiomatic too I'm not sure if we want to encourage to use arbitrary [u8;32]s to construct messages. Imo an API should make it easy to do the right thing and hard to do the wrong thing. Thinking about that this PR should also add a warning to from_slice that refers to the new API. See also this discussion.

If there is ever a common and sane need to sign anything other than a hash we can extend the API. E.g. a Random([u8;32]) struct that can only be constructed using a cryptographically secure RNG and implements ThirtyTwoByteHash would be nice. After thinking about it that's probably a bad idea.

@thomaseizinger
Copy link
Contributor

Imo an API should make it easy to do the right thing and hard to do the wrong thing. Thinking about that this PR should also add a warning to from_slice that refers to the new API.

I tend to agree with that but it is kind of tricky depending on the abstraction level of your library (more low-level APIs tend to be unsafer in their usage). The fact that rust-bitcoin, rust-secp256k1 and bitcoin_hashes are three different crates put some artificial restrictions in place here.
We now have an optional feature to bitcoin_hashes in a crate that is only supposed to deal with an elliptic curve. That is kind of odd, no?
That is why I went with raw byte arrays as that is the lowest common denominator between those crates (assuming we want to keep the current split as is).

Currently, the from_slice function doesn't do any checks apart from the length. From that state, it seems pretty pointless to have as array length is something that can be enforced by the type system.

However, the trait documentation mentions things like all-zero messages and ones that overflow the group order. That sounds like we should actually be doing more checks in the from_slice constructor than what we are doing today?

@sgeisler
Copy link
Contributor Author

sgeisler commented Apr 24, 2020

Having a hashing crate as an optional dependency isn't odd at all since the only sane way to sign anything is to hash it first. This crate is also primarily a BTC crypto lib, so having a tighter integration with a bitcoin specific hash library isn't too bad imo. While secp256k1 the c library is quite low level this crate should have (and has afaik) the goal to be more use friendly and to have safe abstractions.

I see that yet another dependency to be updated isn't ideal, but it's a trade off I'm willing to make for a safe API. Alternatively there could be a separate crate secp256k1-safe that contains the ThirtyTwoByteHash trait and has both bitcoin_hashes and secp256k1 as dependencies and just pub uses the relevant structs from both. But that seems a bit over the top.

@real-or-random
Copy link
Collaborator

real-or-random commented Apr 24, 2020

We now have an optional feature to bitcoin_hashes in a crate that is only supposed to deal with an elliptic curve. That is kind of odd, no?

Who said that it's supposed to deal with an elliptic curve? The README states "rust-secp256k1 is a wrapper around libsecp256k1, a C library by Pieter Wuille for producing ECDSA signatures using the SECG curve secp256k1".

So the main point of this library is to create (and verify) ECDSA signatures. And ECDSA relies on an elliptic curve and a hash function. People tend to forget about the hash function.

Having a hashing crate as an optional dependency isn't odd at all since the only sane way to sign anything is to hash it first.

Worse, it's actually not "signing" if you don't do the hash.

@thomaseizinger
Copy link
Contributor

So the main point of this library is to create (and verify) ECDSA signatures. And ECDSA relies on an elliptic curve and a hash function. People tend to forget about the hash function.

That is a fair point. I guess something like this Digest trait could be useful as-well then the signing part wouldn't be limited to the hash functions defined in bitcoin_hashes.

In any case, this sounds to me like we should just directly remove from_slice? At least in its current state, its existence is inconsistent with the goal of providing safe abstractions.

@elichai
Copy link
Member

elichai commented Apr 26, 2020

That is a fair point. I guess something like this Digest trait could be useful as-well then the signing part wouldn't be limited to the hash functions defined in bitcoin_hashes.

I'm not a big fan of traits like Digest, because I then see people writing higher abstractions, while still making them generic over the trait and in the end it's the user who needs to decide the exact hash function, and there's a bunch of unsafe hash functions that implement this trait.
Worse than that, the user can accidentally "choose" a hash function because of the trait system.

I'm not sure what the best option here is.
IMHO libsecp should've been designed such that it hashes the data on signing/verifying instead of passing an already hashed data, although then some altcoins wouldn't like that because it would limit it to SHA2 only.

@sgeisler
Copy link
Contributor Author

IMHO libsecp should've been designed such that it hashes the data on signing/verifying

The problem with that is (and why this crate shouldn't have an API like that either) that a naive implementation fn sign(sec_key: &SecretKey, msg: &[u8]) -> Signature would require the application to be able to keep the data that has to be signed completely in memory (hard for microcontrollers). If you use something like a signer that impls Write that signer had to keep the intermediate hashing state, an odd combination imo. So having different methods to acquire a hash and then giving it to a sign function while the type system enforces it's hash-ness seems to be at least a local API design optimum.

@thomaseizinger
Copy link
Contributor

while the type system enforces it's hash-ness seems to be at least a local API design optimum.

We are not achieving this as long as from_slice is around.

@sgeisler
Copy link
Contributor Author

I added documentation that makes it pretty clear to normally use the new API and only use from_slice if you know what you are doing.

Copy link
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK.

Is there any opposition to me merging this? I think this is roughly the API we had in mind when we created the ThirtyTwoByteHash, and I think having bitcoin_hashes be an optional non-default dependency of rust-secp is the least ugly way to manage the dependency tree.

Copy link
Member

@elichai elichai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, Why not. as Andrew said this is probably the best compromise

@apoelstra apoelstra merged commit a5147bb into rust-bitcoin:master May 24, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants